今天的更新內容主要是建立一個簡單的網頁伺服器,並透過表單與API來接受使用者的輸入。
我們將一步一步地講解每個部分的設計理念,以及可能存在的安全問題,特別是如何防範 HTML注入、CSS注入、和 JavaScript注入 等漏洞。
docker-compose.ymlservices:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./web:/usr/src/app
      - /usr/src/app/node_modules
    command: sh -c "npm install && npm start"
  mongo:
    image: mongo:latest
command: sh -c "npm install && npm start" 的原因:自動化根據相依性的套件進行安裝,避免因為缺少套件而導致應用程式無法正常執行。sh -c:使用 Shell 執行多個指令。&&:確保前一個指令(npm install)成功執行後,才會執行下一個命令(npm start)。如果安裝失敗,應用程式不會啟動,幫助開發者及早發現問題。npm install 會根據 package.json 安裝所有的相依套件,有一些相依套件根據 postinstall 這些腳本會自動執行,如果相依套件的內容被「惡意竄改」可能包含有害腳本,若在 Docker 容器中執行,可能會導致資安漏洞。root 進行執行,雖然容器可能有進行隔離,但是仍可能有「容器逃逸漏洞」。
web/public/index.html<html>
<head>
  <meta charset="UTF-8">
  <title>歡迎來到測試網頁</title>
</head>
<body>
  <h1>歡迎來到測試網頁</h1>
  <button onclick="fetchMessage()">從 API 取得資訊</button>
  <div id="message"></div>
  進入 <a href="/form">表單頁面</a>
  <script>
    function fetchMessage() {
      fetch('/api/hello')
        .then(response => response.json())
        .then(data => {
          document.getElementById('message').innerText = data.message;
        });
    }
  </script>
</body>
</html>
進入 <a href="/form">表單頁面</a> 連結到表單頁面,讓使用者能夠前往 /form 頁面,進行 GET 和 POST 操作。web/public/form.html<html>
<head>
  <meta charset="UTF-8">
  <title>表單頁面</title>
</head>
<body>
  <h1>表單操作</h1>
  <h2>GET 請求</h2>
  <!-- 搜尋表單 -->
  <form action="/search" method="get">
    <input type="text" name="keyword" placeholder="請輸入關鍵字">
    <input type="submit" value="搜尋">
  </form>
  <h2>POST 表單</h2>
  <!-- 登入表單 -->
  <form action="/submit" method="post">
    <input type="text" name="username" placeholder="帳號">
    <input type="password" name="password" placeholder="密碼">
    <input type="submit" value="送出">
  </form>
</body>
</html>
模擬登入功能,透過POST方式送出帳號和密碼,模擬登入表示尚未串接資料庫和其他驗證帳號密碼的方法。web/server.js(1/3)
// 設定 form
app.get('/form', (req, res) => {
  // 當存取 form 路徑時,請求 form.html 檔案
  res.sendFile(path.join(__dirname, 'public', 'form.html'));
});
使用 Express.js 設定一個 GET 路由,當使用者存取 /form 路徑時,伺服器會傳送位於 public 資料夾中的 form.html 檔案。res.sendFile() 方法用於傳送指定路徑的檔案作為回應。
form.html 包含敏感資訊,直接暴露給所有使用者可能帶來風險。應確保該檔案不含機密資料,或實施適當的存取控制。/form 路徑。如果該表單僅適用於特定使用者,應該加入身份驗證或授權機制。web/server.js(2/3)// 設定搜尋路由
app.get('/search', (req, res) => {
  // 取得查詢字串參數
  const keyword = req.query.keyword;
  // 顯示查詢字串參數
  res.send(`你搜尋的關鍵字是:${keyword}`);
});
這段程式碼設定一個 GET 路由 /search。當使用者在查詢字串中提供 keyword 參數時,伺服器會取得該參數並在回應中顯示。例如,存取 /search?keyword=測試,伺服器會回應「你搜尋的關鍵字是:測試」。
反射型跨站腳本攻擊(Reflected XSS):直接將使用者輸入的 keyword 顯示在回應中,且未經任何過濾或轉義,攻擊者可以藉此插入惡意腳本。例如,存取 /search?keyword=<script>alert('XSS')</script>,可能導致使用者瀏覽器執行惡意腳本。
資訊洩漏:如果回應中包含敏感資訊,可能會被未經授權的第三方攔截或利用。
web/server.js(3/3)// 使用 express.json() 來解析 JSON 格式的請求
app.use(express.json());
// 使用 express.urlencoded() 來解析 URL 編碼的請求
app.use(express.urlencoded({ extended: true }));
// 設定表單提交路由
app.post('/submit', (req, res) => {
  // 取得 POST 請求的參數
  const { username, password } = req.body;
  // 顯示 POST 請求的參數
  res.send(`你的帳號是 ${username},密碼是 ${password}`);
});
這段程式碼使用中介軟體 express.json() 和 express.urlencoded() 來解析請求主體中的 JSON 和 URL 編碼資料。然後,設定了一個 POST 路由 /submit,從請求主體中解構取得 username 和 password,並在回應中顯示這些資訊。
username 和 password 進行任何驗證,可能使系統易受 SQL 注入或其他注入式攻擊的影響。username 或 password 包含惡意腳本,且未經過濾,可能導致 XSS 攻擊。docker-compose up,啟動服務。http://localhost:3000,看到主頁內容。/form,進行GET和POST請求的測試。在接受使用者輸入時,如果未對輸入內容進行適當的驗證和過濾,可能會導致 HTML注入、CSS注入、和 JavaScript注入 等安全漏洞。
例如:
git checkout db6597ae93f1d0e19879c69920021346c3850be7 切換到該 Commit。sudo apt install git
git clone https://github.com/fei3363/ithelp_web_security_2024.git
cd ithelp_web_security_2024/
git checkout db6597ae93f1d0e19879c69920021346c3850be7 切換到該 Commit
index.html)<a> 標籤,可以連到 href 指定的網頁。
form.html)/search。/submit。目標

http://nodelab.feifei.tw/search?keyword=%E6%B8%AC%E8%A9%A6
http://nodelab.feifei.tw/search?keyword=%E6%B8%AC%E8%A9%A6
Protocol: http://
Domain: nodelab.feifei.tw
Path: /search
Query: ?keyword=%E6%B8%AC%E8%A9%A6
Protocol(協定): http://
Domain(網域): nodelab.feifei.tw
nodelab 是子域名,feifei.tw 是主域名。Path(路徑): /search
Query(查詢): ?keyword=%E6%B8%AC%E8%A9%A6
keyword 是參數名稱。%E6%B8%AC%E8%A9%A6 是經過 URL 編碼的參數值。特別說明查詢部分:
%E6%B8%AC%E8%A9%A6 是 URL 編碼後的中文字符「測試」。
POST 是 HTTP 協議中常用的請求方法之一,主要用於向伺服器提交資料。與 GET 方法不同,POST 方法將資料放在請求主體(body)中,而不是 URL 中。

Content-Type: application/x-www-form-urlencoded
Payload (請求主體):
username=admin&password=password
& 分隔。server.js)
輸入: <h1>這是標題</h1>
結果: 所輸入的內容被解析成 HTML

解釋:
輸入: <style>body { background-color: red; }</style>
結果: 頁面背景變為紅色

解釋:
輸入: <script>alert('你被攻擊了!');</script>
結果: 彈出警告框

解釋:
// 使用 he 
const he = require('he');
app.post('/submit', (req, res) => {
  const { username, password } = req.body;
  res.send(`你的帳號是 ${he.encode(username)},密碼是 ${he.encode(password)}`);
});
// 使用 sanitize-html 
const sanitizeHtml = require('sanitize-html');
app.post('/submit', (req, res) => {
  const { username, password } = req.body;
  res.send(`你的帳號是 ${sanitizeHtml(username)},密碼是 ${sanitizeHtml(password)}`);
});
// 使用 DOMPurify  (需要在 Node.js 環境中使用 jsdom)
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
app.post('/submit', (req, res) => {
  const { username, password } = req.body;
  res.send(`你的帳號是 ${DOMPurify.sanitize(username)},密碼是 ${DOMPurify.sanitize(password)}`);
});
he
npm install he
sanitize-html
npm install sanitize-html
DOMPurify
npm install dompurify jsdom
透過本次的教學,我們了解了使用者輸入與輸出的基本操作,以及可能存在的安全問題。在未來的學習中,我們將深入探討如何加強應用程式的安全性,防範各種潛在的攻擊。
git clone https://github.com/fei3363/ithelp_web_security_2024.git
git checkout db6597ae93f1d0e19879c69920021346c3850be7
<h1>這是一個測試</h1>
<script>alert('XSS 測試')</script>
完成上述任務後,嘗試實施一些基本的安全措施。例如: